Асинхронное программирование лекция

📄 Thread ThreadPool

⚙ Thread in General

Untitled.png

Могут существовать два вида операций
Обычные вычисления и IO операции
IO операции требуют много времени на ожидание

Статусы Тредов:

  • Ready - готов к выполнению
  • Running - выполняется на CPU
  • Waiting - ожидает завершения IO операции или lock
  • Standby, Terminated, Initialized

Native thread - это потоки, созданные и управляемые непосредственно операционной системой.


⚙ Thread in dotnet

  • Foreground - - при закрытии приложение будет ждать окончание работы таких thread’s
  • Background - в случае закрытия приложения, такие треды останавливаются

Существует thread API

 Thread th = new Thread(...);
 
// вот здесь появляется native thread
th.Start(arg);
// Контекст свитчнется к другому треду, но этот тред продолжит выполнение на том же CPU.
// Оптимизация в том, что все будет в кэше
Thread.Yeild();

// Вызовет микропаузу на процессоре.
// Если есть како-то тред в рэйди статусе, то контекст переключится
// Если другого такого треда нет, то переключение не произойдет
Thread.Sleep(0)

SpinWait в .NET — это структура, предоставляющая механизм для активного ожидания (busy-waiting) в многопоточном программировании. Вместо того чтобы блокировать поток и вызывать переключение контекста, как это делает Thread.Sleep или синхронизационные примитивы, такие как мьютексы или семафоры, SpinWait удерживает поток активным, выполняя циклы ожидания. Это полезно в сценариях, где ожидаемое время ожидания короткое, и переключение контекста может быть более затратным, чем активное ожидание.


⚙ ThreadPool in General

CPU-bound задачи ограничены в своей производительности мощностью процессора. Это означает, что для выполнения таких задач требуется интенсивная работа процессора, и они выполняются быстрее, если использовать более быстрый или более мощный процессор. Примеры CPU-bound задач включают сложные вычисления, такие как численные методы, рендеринг графики, шифрование и декодирование данных.

I/O-bound задачи ограничены скоростью ввода-вывода (I/O), то есть они зависят от скорости доступа к диску, сети или другим периферийным устройствам. Такие задачи часто ожидают завершения операций ввода-вывода, и их производительность не увеличится значимо с более мощным процессором. Примеры I/O-bound задач включают чтение и запись файлов, сетевое взаимодействие и доступ к базам данных.

типы тредов:

  • worker threads - те которые выполняют непосредственно код
  • io completion port threads (IOCP) - те, которые выполняют задачи по вводу/выводу, async, на уровне OS
  • (Еще есть UI, но опутим)

IOCP работают поверх:

  • IO Completion port (win)
  • epoll (unix)

Виды очередей:

  • global queue - создается одан на весь ThreadPool
  • local queue - создается одна на каждый worker thread в ThreadPool

Алгоритм работы треда:

  1. Он возьмет задачу из своей локальной очереди, при чем ту что меньше всех ждала, пришла последней.
  2. Если ничего нет, он возьмет задачу из глобальной очереди, при чем теперь ту которая больше всех ждала.
  3. Если и там ничего нет, то он украдет задачу у локальной очереди другого треда

📄 Async internals

⚙ Task states

  • RanToConnection - успешно завершен.
  • Faulted - завершен с необработанным исключением.
  • Canceled - отменен.
  • Created, WaitingForActivation, WaitingToRun - Task уже создан, но еще не запланирован к выполнению.
  • Running - запущен и выполняется.
  • WaitingForChildrenToComplete - ожидает завершения всех присоединенных дочерних Task’ов.

⚙ Замыкание

Во время замкания, для захвата переменной, компилятор, в месте где первоначально объявляется переменная создает объект класса, в котором будет один метод, тот самый делегат и поле которое будет захватывать перменную. Далее, вместо работы с самой переменной будет происходить работа с полем внутри этого объекта содержащего делегат.


⚙ Task API

как из создавать

  1. var task = new Task(DoSomething); task.Start(); - Legasy
  2. var task = Task.Factory.StartNew(DoSomething); -
  3. var task = Task.Run(DoSomething);

Многопоточность - это когда "много потоков" - почти что отдельных процессов, но использующих общую память с основным, а асинхронность - когда поток-процесс всего один, но он выполняет разные задачи по очереди с переключениями в моменты ожиданий задачами ввода-вывода или результатов выполнения другой задачи/функции.


⚙ Async / Await

использование этих штук просто перерабатывает метод в стэйт машину.

Создается класс в режиме дебага или структура в режиме релиза, которая реализует интерфейс IAsyncStateMachine

Создается объект AsyncTaskMethodBuilder

В нем вызывается метод Start и передается наша стэйт машина

В классе стэйт машины будет один метод MoveNext() который будет исполнять наш код по состояниям. Состояния разделяются с помощью await. В стэйт машине на месте эвэйт будет происходить проверка закончилась ли таска, которую мы ждем с помощью await. Если не закончилась, то стейт машина запоминает на каком она состоянии и передает в AsyncTaskMethodBuilder ссылку на себя и на таску которую надо асинхронно ждать.

Далее, когда таска выполняется, билдер снова запускает стейт машину до следующего состояния. Так продолжается пока MoveNext полноценно не завершится

  • Async/await - не про скорость, а про масштабируемость
  • IAsyncStateMachine в Релиз режиме - структура, в Дебаг - класс
  • Текущий стейт описывается интовой переменной. Создается IAsyncStateMachine со стейтом -1
  • После создания стейт машины, она запускается на выполнение.
  • Worker threads в ThreadPool освобождаются и могут выполнять дополнительную работу.

По терминологии: рассматриваемый асинхронный метод, чей код будет рассматриваться, я буду называть асинхронный метод, а вызываемые асинхронные методы внутри него я буду называть асинхронная операция.

Блокировка потока означает, что поток из пула потоков будет ожидать когда завершиться метод и этот поток не сможет работать в других метах. Стэйт машина позволяет сохранить состояние и отпустить поток. Join - блокирует поток.

Исключения ловятся в билдере и для этого используется возврат Task


В данном коде, метод Do завершится не ожидая DoAsync так как нет await или GetAwaiter().GetResult()

То есть, стейтмашина создается, запускается, но её выполнения никто не ждет

Pasted image 20240910215424.png


⚙ SynchronizationContext и ConfigurationAwait в C# и TaskSheduler ?????

ConfigurationAwait можно вызывать у объекта таски и передавать фолс, это будет означать для ОС, что контекст подтаскивать не обязательно, в ASP это уже по умолчанию включено


Нюансы

  • async void не используем, т.к. исключения не пробросятся через это
  • ValueTask
    • Task - класс, а ValueTask - это структура
    • ValueTask можно преобразовать в обычный Task
    • ValueTask - нужен когда надо ускорять работу

Чтобы работало await достаточно реализовать у класса метод GetAwaiter(), таким образом можно эвэитить даже инт.